Skip to content

feat(mobile): push notifications MVP - #6269

Merged
brow merged 43 commits into
mainfrom
codex/ios-push-internal
Aug 28, 2026
Merged

feat(mobile): push notifications MVP#6269
brow merged 43 commits into
mainfrom
codex/ios-push-internal

Conversation

@brow

@brow brow commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

This PR implements MVP, iOS-only, NIP-PL-compliant push notifications.

A relay with BUZZ_PUSH_ENABLED will send a push notification for any message that appears in the in-app Notifications tab.

Enrollment flow

The first time the client first connects to a relay with BUZZ_PUSH_ENABLED:

sequenceDiagram
    autonumber
    participant App as Buzz iOS app
    participant iOS
    participant Relay as Buzz relay
    participant Attest as Apple App Attest
    participant Gateway as Push gateway

    App->>Relay: Fetch NIP-11 push capability
    Relay-->>App: Push profile, current relay public key, and limits

    par
        App->>iOS: Request notification permission
        iOS-->>App: Permission result
    and
        App->>iOS: Register for remote notifications
        iOS-->>App: Device token
    end

    App->>Gateway: Request installation challenge
    Gateway-->>App: Single-use challenge
    App->>Attest: Attest installation transcript
    Attest-->>App: Attestation proof
    App->>Gateway: Enroll device token and proof
    Gateway-->>App: Installation handle

    App->>Gateway: Request delegation challenge
    Gateway-->>App: Single-use challenge
    App->>Attest: Assert relay-key delegation
    Attest-->>App: Assertion
    App->>Gateway: Create delegation
    Gateway-->>App: Opaque endpoint grant

    App->>Relay: Publish encrypted push lease and filters
    Relay-->>App: Lease acknowledged
Loading

Push-time flow

When a notification-eligible event is received by the relay:

%%{init: {
  "sequence": {
    "actorMargin": 20,
    "width": 110,
    "messageMargin": 18,
    "diagramMarginX": 8,
    "wrap": true
  }
}}%%
sequenceDiagram
    autonumber
    participant Relay as Buzz relay
    participant Gateway as Push gateway
    participant APNs as Apple Push<br/>Notification service
    participant iOS
    participant NSE as Notification service<br/>extension

    Relay->>Gateway: POST /v1/deliveries/apns<br/>opaque endpoint grant, request ID, expiry, NIP-98 authorization

    Gateway->>APNs: POST /3/device/{device-token}<br/>topic, request ID, expiry, constant mutable-content payload
    APNs-->>Gateway: 200 OK: request accepted
    Gateway-->>Relay: 200 OK: accepted status

    APNs-->>iOS: Notification: constant reconnect alert<br/>mutable-content = 1
    iOS->>NSE: Invoke extension<br/>original notification content

    NSE->>Relay: POST /query: subscription filters, limit 10<br/>NIP-98 authorization
    Relay-->>NSE: 200 OK: signed Nostr events<br/>kinds 9, 40002, 45001, or 45003

    NSE->>iOS: Complete notification: title, body, subtitle<br/>thread ID, exact-message target
Loading

relay → push gateway → APNs -> NSE -> Notification Center

Known limitations

The APNs wake payload is intentionally constant and opaque: it contains no originating community or message identifier, in keeping with the implemented NIP-PL privacy design.

The Notification Service Extension must therefore reconnect to the relay and resolve eligible messages after each wake. Around overlapping wakes, timing boundaries, or resolution windows, notification presentation may occasionally omit an expected message or display a message more than once.

This best-effort behavior is deliberately accepted for the current implementation and will be measured during the internal rollout to determine whether the user experience is acceptable before any broader deployment; the implementation does not claim exactly-once presentation.

Validation

Live end-to-end hardware validation used an internal remotely hosted development relay and push gateway, the APNs sandbox, and a physical iPhone 12 mini:

  • A second real Buzz client published a uniquely marked message through the hosted relay.
  • The relay matched the message and sent the constant opaque wake through the hosted gateway. The gateway made an actual APNs request; no simctl push or simulated notification was used.
  • The iPhone received the notification on its lock screen. The Notification Service Extension reconnected to the relay, fetched the event, verified its ID and signature, and replaced the placeholder content with the real notification title and body.
  • After the app populated its shared presentation cache, a final marked notification visibly showed the sender display name, sender avatar, and hashtag-prefixed channel name.
  • Tapping a lock-screen notification opened Buzz and exercised the notification-response path and navigated to the corresponding message.

Final validation with a dogfood-signed artifact and production App Attest/APNs configuration remains a release step.

Independent pre-reviews

  • First pass: Carl found missing transient retries, executor-key rotation suppression, duplicate installation renewal, and an unauthenticated challenge write amplifier. These were resolved by retry-safe bootstrap and authenticated renewal plus a cross-replica quota. sol-max found delegation generation burning and an edited applied migration, resolved by exact-generation revocation and a forward-only migration. k3-max found no blockers.
  • Exact-head re-review: Carl, sol-max, and k3-max independently returned NO BLOCKERS at 7eb3a650b; k3-max also revalidated every remediation and the endpoint-specific App Attest enrollment bound.

Signed-off-by: Tom Brow <tomb@squareup.com>
@brow
brow force-pushed the codex/ios-push-internal branch from c7a089a to d69b89c Compare August 18, 2026 23:25
@brow brow changed the title Add internal iOS push notification MVP feat(mobile): push notifications MVP Aug 19, 2026
brow added 3 commits August 20, 2026 13:48
Signed-off-by: Tom Brow <tomb@block.xyz>
…ifications-pr

* origin/main: (33 commits)
  fix(hooks): scope pre-push lanes to branch merge-base diff (#6423)
  Enforce a three-day dependency cooldown (#6426)
  perf(desktop): resolve references without directory scans (#6328)
  feat(llm): stamp thinking effort on call-completed log line (#6424)
  Fix cross-owner relay agent mentions in owner-only builds (#6338)
  feat(cli): accept Buzz message links for thread reads (#6359)
  feat(workflows): add workflow editor (#6248)
  fix(desktop): preserve huddle speech boundaries (#6397)
  test(desktop): use a wordlist-safe separator in passphrase word-count test (#6356)
  fix(models): curate Databricks alias-aware labels for 5 missing endpoints (#6360)
  fix(acp): guard against unrequested public relay skills (#6394)
  feat(desktop): refine context-aware Projects collaboration (#6396)
  fix(desktop): distinguish duplicate agent devices (#6337)
  feat(desktop): close Buzz window with Cmd+W (#6314)
  refactor(prompt): simplify Buzz agent guidance (#6340)
  feat(desktop): make Projects workspaces selectable (#6368)
  Add Buzz-native collaboration benchmarks (#6264)
  Polish mobile timeline and emoji interactions (#6297)
  feat(desktop): make the Projects overview follow the selected section (#6335)
  refactor(desktop): coordinate TTS playback (#6341)
  ...

Signed-off-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
@lezdoors

This comment was marked as outdated.

@lezdoors

This comment was marked as outdated.

brow and others added 20 commits August 21, 2026 21:09
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
…ifications-pr

* origin/main: (38 commits)
  fix(composer): preserve caret when inserting mentions mid-message (#6531)
  chore(deps): update rust crate async-trait to v0.1.92 (#6094)
  chore(deps): update dependency sonner to v2.0.8 (#6093)
  chore(deps): update rust crate http-body-util to v0.1.4 (#5452)
  chore(deps): update rust crate http to v1.4.2 (#5451)
  chore(deps): update rust crate futures-util to v0.3.33 (#5448)
  chore(deps): update rust crate futures to v0.3.33 (#5445)
  chore(deps): update dependency @tauri-apps/api to v2.11.1 (#5444)
  chore(deps): update ubuntu:24.04 docker digest to 561618e (#5442)
  chore(deps): update swatinem/rust-cache digest to 6323deb (#5441)
  fix(desktop): restore true zoom by scaling the root rem (#6514)
  chore(desktop): drop unused ORIGINAL_CONTENT from empty-edit-delete spec (#6517)
  feat(workflows): clarify workflow setup and activation (#6470)
  perf(desktop): stop the Projects fan refetching on re-entry and running after leave (#6458)
  perf(desktop): keep the member roster off the channel-switch path (#6456)
  fix(deletion): allow IRSA S3 credentials (#6495)
  docs(nips): comprehensive NIP-FI — core + claimable profiles (EDGE/LIFECYCLE/DELEG/CONF) (#5946)
  fix(benchmarks): wait for scripted event delivery (#6487)
  Polish mobile channel navigation and message sends (#6488)
  Clarify huddle message destination (#6496)
  ...

Signed-off-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
Signed-off-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
…ifications-pr

* origin/main:
  fix(desktop): emit singular `mention` feed category so alerts route correctly (#6665)
  fix(mobile): recover stale and shuffled messages (#6691)
  feat(mobile): browse and join open channels (#6243)
  show mention counts in channel notifications (#6696)
  fix(desktop): hide selection formatting tray on composer right-click (#6683)
  fix(desktop): stabilize members dialog scrolling (#6670)
  fix(desktop): keep member runtime status off the UI thread (#6445)
  perf(desktop): persist channel heads, collapse thread reads and reply sends (#6572)
  Downgrade desktop Huddles to audio protocol v2 (#6610)
  Polish Huddle participant interactions (#6312)
  Downgrade mobile Huddles to audio protocol v2 (#6558)
  perf(desktop): make the Projects surface render-cheap (#6460)
  refactor(acp): clarify agent prompt sections (#6501)
  Add mobile Huddles voice MVP (#6056)
  feat(desktop-messages): keep agents addressed across messages (#6315)
  fix(desktop): remove Buzz entity link previews (#6512)

Signed-off-by: Tom Brow <tomb@block.xyz>

# Conflicts:
#	mobile/lib/features/channels/channels_provider.dart
#	mobile/lib/shared/auth/auth_provider.dart
#	mobile/lib/shared/community/community_provider.dart
#	mobile/test/shared/auth/auth_provider_test.dart
#	mobile/test/shared/community/community_provider_test.dart
#	scripts/mobile-worktree-overrides.sh
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
Signed-off-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
…ifications-pr

* origin/main:
  Centralize replaceable event persistence (#6660)
  feat(workflows): discover trigger filter values (#6712)
  feat(desktop): simplify the message action rail (#6529)
  fix(desktop): restore icon-only remote marker (#6491)
  fix(ci): prevent poisoned Rust caches (#6618)
  docs(security): route reports through private advisories (#6728)
  fix(composer): wrap Buzz chip labels without orphaning icons (#6581)
  fix(desktop): bound thread /query and surface load errors, not false-empty (#6447)

Signed-off-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
Signed-off-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
@brow
brow marked this pull request as ready for review August 26, 2026 02:41
@brow
brow requested a review from a team as a code owner August 26, 2026 02:41
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is a3730784fc851bb1125b40cca9b0a30788a293c1...a0eaf8c8b2bbec08d504d19051b036edcbc1a2d9.
A new review must complete for this exact range. When manual authorization
is required, a Block organization member must comment exactly
@buzz-security-review a0eaf8c8b2bbec08d504d19051b036edcbc1a2d9 to authorize a new review.
Any previous review applies only to its recorded range.

@brow
brow dismissed jedwards27’s stale review August 27, 2026 19:15

blockers addressed

@brow
brow requested a review from jedwards27 August 27, 2026 19:15
Signed-off-by: Tom Brow <tomb@block.xyz>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carl, an automated reviewer, commenting via Wes’s GitHub account.

REQUEST CHANGES at exact head 4c333cdea63ecf6aa57764c4d2521fb8d952322e (base 86b9142a09f2af3ba2fff7effa6a6cd53b40f51c).

Blocking finding

APNs topic/environment misconfiguration permanently disables valid endpoint generations

crates/buzz-push-gateway/src/apns.rs:40-43 classifies both BadDeviceToken and DeviceTokenNotForTopic as InvalidEndpoint. Apple documents that BadDeviceToken can mean the token does not match the selected APNs environment, while DeviceTokenNotForTopic means the token does not match the configured topic. Those are not necessarily dead device tokens; they can be deployment profile faults.

This PR permits exactly those mismatches: AppProfileConfig accepts the App Attest app ID, APNs certificate, topic, and environment as independent environment values (config.rs:12-16,85-109). Constructing reqwest::Identity validates the certificate/key pair, but nothing validates that its topic/environment match the attested dogfood app before traffic starts.

The resulting failure crosses a destructive boundary:

  1. the gateway turns either APNs 400 into InvalidEndpoint and returns HTTP 410 (http.rs:739-746);
  2. the relay consumes 410 and calls disable_push_endpoint for the current lease generation (push_runtime.rs:545-570);
  3. the DB permanently clears endpoint_enabled for that active generation (buzz-db/src/push.rs:1222-1244).

A wrong topic, certificate scope, or sandbox/production selection can therefore disable every otherwise-valid endpoint it touches. Fixing deployment configuration does not re-enable those current generations, and the failure is counted as endpoint invalidation rather than tripping the configuration-fault path the runbook relies on.

Required change: do not map configuration-ambiguous APNs responses across the permanent endpoint-disable boundary. At minimum, classify DeviceTokenNotForTopic as ConfigurationFault; handle BadDeviceToken so an environment mismatch cannot mass-disable endpoints (for example, conservatively classify it as configuration fault unless the endpoint can be independently proven invalid). Add an end-to-end classification regression proving these outcomes return retryable/configuration failure and never disable the relay lease generation. Keep permanent invalidation for APNs 410 Unregistered, which explicitly states that no further pushes are needed.

Apple reference: https://developer.apple.com/documentation/usernotifications/handling-notification-responses-from-apns

Review coverage

The team traced mobile enrollment/revocation persistence, iOS presentation/navigation, relay authorization and generation fencing, DB trigger/migration parity, and the message-kind closed set {9, 40002, 45001, 45003}. No additional blockers were verified. git diff --check passed; the full BuzzPushKit suite passed (86 tests, 0 failures); the focused gateway classification test passed but does not cover either dangerous 400 reason. Required CI was still running at publication, so this verdict is source-established rather than CI-dependent. Physical-device APNs/App Attest delivery remains release-owned validation.

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
@brow

brow commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

@buzz-security-review 3401e59

wesbillman

This comment was marked as resolved.

@brow
brow requested a review from wesbillman August 28, 2026 17:55

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carl, an automated reviewer, commenting via Wes’s GitHub account.

APPROVE at exact head 3401e595ad31ebd5868926d777d307ad3fa3612b against PR base 86b9142a09f2af3ba2fff7effa6a6cd53b40f51c.

I re-reviewed this with independent mobile UX, iOS bridge, and end-to-end contract lanes. The prior APNs blocker is fixed: BadDeviceToken and DeviceTokenNotForTopic remain retryable configuration faults, while only APNs 410 Unregistered crosses the endpoint-disable boundary. Supported push kinds remain consistent across relay, migration, client, and NSE consumers: {9, 40002, 45001, 45003}.

I am also withdrawing my later permission-refresh objection. A focused iOS simulator probe reproduced first-prompt denial: the system prompt deactivated the app, requestAuthorization completed with granted=false, and the app resumed. Flutter maps those UIKit lifecycle notifications to inactive → resumed; this PR’s provider refreshes on that resumed transition, so the denied Settings recovery state is reached. The missing direct completion callback would improve explicitness, but is not a verified user-visible defect.

Validation: required GitHub checks are green at this head, git diff --check is clean, and the focused simulator lifecycle probe passed. Production-signed physical-device APNs/App Attest and the full lock-screen/Focus/VoiceOver lifecycle matrix remain release validation, not source merge blockers.

GitHub currently reports the branch as conflicting with the repository’s latest main (ed11c8d8bf0a17402be5cf243724f89471530d2f), which advanced beyond the PR base. Resolve/rebase that mergeability issue before merge; it is not a defect in the reviewed push implementation.

@lezdoors

Copy link
Copy Markdown

Hi @brow — following this closely, great to see it approved. I run a self-hosted Buzz relay (buzz.lmiere.com) with push enabled on the relay side, and a small team using it daily. The relay half is ready; the App Store build has no push code yet, so nothing rings.

The PR mentions an internal rollout before broader deployment. Any chance of joining it, via TestFlight or an internal build? Happy to report back on how push behaves against a self-hosted relay — probably a setup worth having tested before public release.

Thanks for the work on this.

Signed-off-by: Tom Brow <tomb@block.xyz>
@brow
brow enabled auto-merge (squash) August 28, 2026 19:23
Signed-off-by: Tom Brow <tomb@block.xyz>
@brow
brow requested a review from wesbillman August 28, 2026 20:54
@brow
brow merged commit c432a11 into main Aug 28, 2026
46 checks passed
@brow
brow deleted the codex/ios-push-internal branch August 28, 2026 21:15
delkc added a commit that referenced this pull request Aug 28, 2026
…rding-v3

* origin/main:
  feat(mobile): push notifications MVP (#6269)

Signed-off-by: Clay Delk <clay.delk@gmail.com>
wpfleger96 pushed a commit that referenced this pull request Aug 28, 2026
…-history

* origin/main:
  feat(mobile): push notifications MVP (#6269)
  refactor(db): extract domain stores from database runtime (#6987)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
wpfleger96 pushed a commit that referenced this pull request Aug 28, 2026
main landed 0040_push_message_kinds.sql (#6269) which collides with the
previous NIP-FI numbering. Renumber:
  0040_nip_fi_identity_foundation.sql   → 0041
  0041_nip_fi_authorization_foundation.sql → 0042

Update all test references, run_to() calls, and schema.sql comments to
match. The push_match_trigger test (migrations[39].version == 40) is
unchanged — it covers the push-notification migration at 0040, not NIP-FI.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
wpfleger96 pushed a commit that referenced this pull request Aug 28, 2026
* origin/main:
  fix(desktop): resolve bundled sidecar on cheap path and bound login-shell spawns (#6904)
  perf(mobile): reduce cold startup and channel rendering delays (#6996)
  feat(mobile): push notifications MVP (#6269)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
wpfleger96 pushed a commit that referenced this pull request Aug 28, 2026
…enericize

* origin/main:
  fix(desktop): resolve bundled sidecar on cheap path and bound login-shell spawns (#6904)
  perf(mobile): reduce cold startup and channel rendering delays (#6996)
  feat(mobile): push notifications MVP (#6269)
  refactor(db): extract domain stores from database runtime (#6987)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
wpfleger96 pushed a commit that referenced this pull request Aug 28, 2026
…agent-edit

* origin/main:
  fix(desktop): resolve bundled sidecar on cheap path and bound login-shell spawns (#6904)
  perf(mobile): reduce cold startup and channel rendering delays (#6996)
  feat(mobile): push notifications MVP (#6269)
  refactor(db): extract domain stores from database runtime (#6987)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
jrobotham-square added a commit that referenced this pull request Aug 29, 2026
…age-rw

* origin/main:
  fix(desktop): resolve bundled sidecar on cheap path and bound login-shell spawns (#6904)
  perf(mobile): reduce cold startup and channel rendering delays (#6996)
  feat(mobile): push notifications MVP (#6269)
  refactor(db): extract domain stores from database runtime (#6987)
  feat(desktop): add team sharing to community catalog (#3995)
  Refresh mobile utility surfaces and theme picker (#6944)
  fix(desktop): complete project empty and context states (#6980)
  Fix mobile jump-to-latest flicker (#6807)
  refactor(relay): NIP-98 admin auth with Operator/Moderator roles and NIP-11 discovery (#3777)
  refactor(db): split channel membership store (#6782)
  feat(auth): add NIP-FI canonical assertion verifier and contracts (#6776)

Signed-off-by: Joel Robotham <jrobotham@squareup.com>
@brow

brow commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

The PR mentions an internal rollout before broader deployment. Any chance of joining it, via TestFlight or an internal build? Happy to report back on how push behaves against a self-hosted relay — probably a setup worth having tested before public release.

@lezdoors Sounds great, thank you. I need to land some additional changes to the push gateway before that will be possible, but I'll let you know when it's ready.

In the meantime, email me at tomb@block.xyz, and I'll get you onto our TestFlight.

wpfleger96 pushed a commit that referenced this pull request Aug 29, 2026
This PR implements MVP, iOS-only,
[NIP-PL](https://github.com/block/buzz/blob/8d2d0ff5ad42733e9949442c4b6358d0ba87f9a8/docs/nips/NIP-PL.md)-compliant
push notifications.

A relay with `BUZZ_PUSH_ENABLED` will send a push notification for any
message that appears in the in-app Notifications tab.

## Enrollment flow
The first time the client first connects to a relay with
`BUZZ_PUSH_ENABLED`:
```mermaid
sequenceDiagram
    autonumber
    participant App as Buzz iOS app
    participant iOS
    participant Relay as Buzz relay
    participant Attest as Apple App Attest
    participant Gateway as Push gateway

    App->>Relay: Fetch NIP-11 push capability
    Relay-->>App: Push profile, current relay public key, and limits

    par
        App->>iOS: Request notification permission
        iOS-->>App: Permission result
    and
        App->>iOS: Register for remote notifications
        iOS-->>App: Device token
    end

    App->>Gateway: Request installation challenge
    Gateway-->>App: Single-use challenge
    App->>Attest: Attest installation transcript
    Attest-->>App: Attestation proof
    App->>Gateway: Enroll device token and proof
    Gateway-->>App: Installation handle

    App->>Gateway: Request delegation challenge
    Gateway-->>App: Single-use challenge
    App->>Attest: Assert relay-key delegation
    Attest-->>App: Assertion
    App->>Gateway: Create delegation
    Gateway-->>App: Opaque endpoint grant

    App->>Relay: Publish encrypted push lease and filters
    Relay-->>App: Lease acknowledged
```

## Push-time flow

When a notification-eligible event is received by the relay:

```mermaid
%%{init: {
  "sequence": {
    "actorMargin": 20,
    "width": 110,
    "messageMargin": 18,
    "diagramMarginX": 8,
    "wrap": true
  }
}}%%
sequenceDiagram
    autonumber
    participant Relay as Buzz relay
    participant Gateway as Push gateway
    participant APNs as Apple Push<br/>Notification service
    participant iOS
    participant NSE as Notification service<br/>extension

    Relay->>Gateway: POST /v1/deliveries/apns<br/>opaque endpoint grant, request ID, expiry, NIP-98 authorization

    Gateway->>APNs: POST /3/device/{device-token}<br/>topic, request ID, expiry, constant mutable-content payload
    APNs-->>Gateway: 200 OK: request accepted
    Gateway-->>Relay: 200 OK: accepted status

    APNs-->>iOS: Notification: constant reconnect alert<br/>mutable-content = 1
    iOS->>NSE: Invoke extension<br/>original notification content

    NSE->>Relay: POST /query: subscription filters, limit 10<br/>NIP-98 authorization
    Relay-->>NSE: 200 OK: signed Nostr events<br/>kinds 9, 40002, 45001, or 45003

    NSE->>iOS: Complete notification: title, body, subtitle<br/>thread ID, exact-message target
```

relay → push gateway → APNs -> NSE -> Notification Center

## Known limitations

The APNs wake payload is intentionally constant and opaque: it contains
no originating community or message identifier, in keeping with the
implemented NIP-PL privacy design.

The Notification Service Extension must therefore reconnect to the relay
and resolve eligible messages after each wake. Around overlapping wakes,
timing boundaries, or resolution windows, notification presentation may
occasionally omit an expected message or display a message more than
once.

This best-effort behavior is deliberately accepted for the current
implementation and will be measured during the internal rollout to
determine whether the user experience is acceptable before any broader
deployment; the implementation does not claim exactly-once presentation.

## Validation

Live end-to-end hardware validation used an internal remotely hosted
development relay and push gateway, the APNs sandbox, and a physical
iPhone 12 mini:

- A second real Buzz client published a uniquely marked message through
the hosted relay.
- The relay matched the message and sent the constant opaque wake
through the hosted gateway. The gateway made an actual APNs request; no
`simctl push` or simulated notification was used.
- The iPhone received the notification on its lock screen. The
Notification Service Extension reconnected to the relay, fetched the
event, verified its ID and signature, and replaced the placeholder
content with the real notification title and body.
- After the app populated its shared presentation cache, a final marked
notification visibly showed the sender display name, sender avatar, and
hashtag-prefixed channel name.
- Tapping a lock-screen notification opened Buzz and exercised the
notification-response path and navigated to the corresponding message.

Final validation with a dogfood-signed artifact and production App
Attest/APNs configuration remains a release step.

## Independent pre-reviews

- **First pass:**
[Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=74ab9a93bb227f3e762568f1cf9fee66d7495b0edc3918735ff787238b9cc585)
found missing transient retries, executor-key rotation suppression,
duplicate installation renewal, and an unauthenticated challenge write
amplifier. These were resolved by [retry-safe
bootstrap](12c66ea62)
and [authenticated renewal plus a cross-replica
quota](8e5ece0bd).
[sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=d26d53daa4684669e2ed354638241f13f36c3a97027fe8b4dd738aff09038962)
found delegation generation burning and an edited applied migration,
resolved by [exact-generation
revocation](c26d2159d)
and a [forward-only
migration](956c1d099).
[k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=d43139138a0b15f806cbdbeeedd8f69d992cadf2e805876db6fdde6a34c7eda1)
found no blockers.
- **Exact-head re-review:**
[Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=a897721673459301b0cf26e8b85a1478d7ebbb56a4621f93d774c98d395b8f68),
[sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=fb2159f709ec68f74f7b21459acd76da0e8a7c5c0f3d469f99826b0cc2380849),
and
[k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=2ce2842910435f562e9d9cc718595848f281b122c94605e523a4b964254b8bfb)
independently returned **NO BLOCKERS** at `7eb3a650b`; k3-max also
revalidated every remediation and the endpoint-specific App Attest
enrollment bound.

---------

Signed-off-by: Tom Brow <tomb@squareup.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Jordan Mecom <jm@squareup.com>
wpfleger96 pushed a commit that referenced this pull request Aug 29, 2026
…c-agent-commit-identity

* origin/main:
  fix(desktop): surface channel history load failures (#7013)
  fix(composer): polish automatic mentions (#6956)
  fix(desktop): resolve bundled sidecar on cheap path and bound login-shell spawns (#6904)
  perf(mobile): reduce cold startup and channel rendering delays (#6996)
  feat(mobile): push notifications MVP (#6269)
  refactor(db): extract domain stores from database runtime (#6987)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants